Skip to content

fix(generate-types): re-sync hardcoded TS output with contracts.ts + drift test#472

Closed
electrolobzik wants to merge 2 commits into
smart-mcp-proxy:mainfrom
HaloCollar:upstream-fix/build-churn
Closed

fix(generate-types): re-sync hardcoded TS output with contracts.ts + drift test#472
electrolobzik wants to merge 2 commits into
smart-mcp-proxy:mainfrom
HaloCollar:upstream-fix/build-churn

Conversation

@electrolobzik
Copy link
Copy Markdown
Contributor

@electrolobzik electrolobzik commented May 16, 2026

Summary

The TypeScript-as-Go-string literal inside cmd/generate-types/main.go has drifted from the actual frontend/src/types/contracts.ts it claims to generate. Running go run ./cmd/generate-types (invoked by the Makefile's frontend-build target on every make build / make build-server) silently reverts a chunk of contracts.ts, producing a dirty working tree after every build.

The drift was introduced by two prior PRs that hand-edited contracts.ts without updating the generator:

Neither PR touched cmd/generate-types/main.go. The banner at the top of contracts.ts says DO NOT EDIT - This file is auto-generated by cmd/generate-types, which made it natural to assume re-running the generator would just produce those fields. It doesn't — the generator's output is hand-maintained Go string literals, so manual edits to contracts.ts need a matching update to the generator or the next build silently reverts them.

Changes

1. Sync the generator (cmd/generate-types/main.go)

Catches the hardcoded TS string literals up to the current contracts.ts content. Adds the six missing fields and the IsolationDefaults interface so the generator's output is byte-identical to the committed contracts.ts.

Also refactors main() to extract generateFileContent() so the new test can validate the output without re-implementing the header concat.

2. Drift-prevention test (cmd/generate-types/main_test.go)

TestContractsInSync reads the committed frontend/src/types/contracts.ts, computes what cmd/generate-types would write today, and asserts byte-equality. The next time someone hand-edits one side without updating the other, CI fails with a clear message:

Either run go run ./cmd/generate-types from the module root (if the generator is the source of truth) or update the string literals in main.go (if contracts.ts is the source of truth).

This is the smallest discipline that would have caught both #424 and #463 before merge.

What this PR is not

This PR fixes the contracts.ts half of the dirty-tree problem only. The committed web/frontend/dist/* bundle also churns on every make build, but for an independent reason: the bundle in main is older than the frontend source it claims to be built from (last refresh was in #433 wizard v2; subsequent feature PRs #424 / #463 added Vue code but didn't refresh the bundle), so Vite produces different content-hashed filenames than the ones committed. That deserves its own discussion (PLACEHOLDER + embed all: vs deterministic re-bundle vs leave-as-tracked-artifact) and a separate PR.

Verification

$ go test ./cmd/generate-types/
ok  	github.com/smart-mcp-proxy/mcpproxy-go/cmd/generate-types	0.006s

$ go run ./cmd/generate-types
Successfully generated TypeScript types: frontend/src/types/contracts.ts
$ git diff frontend/src/types/contracts.ts
(empty)

$ go build ./...
(succeeds)

Sanity-checked drift detection by appending one line to contracts.ts and re-running go test ./cmd/generate-types/ — test fails with the expected message, then passes again after reverting.

Test plan

  • go test ./cmd/generate-types/ passes
  • go run ./cmd/generate-types is idempotent against committed contracts.ts
  • Test catches a simulated drift (appended line to contracts.ts → test fails as designed)
  • go build ./... succeeds

🤖 Generated with Claude Code

The TypeScript-as-Go-string literal in cmd/generate-types/main.go drifted
from frontend/src/types/contracts.ts when PR smart-mcp-proxy#424 (Server Config tab
parity) and PR smart-mcp-proxy#463 (per-tool enable/disable) edited contracts.ts
directly without updating the generator. Running `go run ./cmd/generate-types`
(invoked by Makefile's `frontend-build` target) silently reverts those
fields, producing a dirty working tree on every `make build`:

  - Server.isolation_defaults
  - IsolationConfig.network_mode, IsolationConfig.extra_args
  - IsolationDefaults (entire interface)
  - Tool.disabled, Tool.approval_status

The reverted contracts.ts also feeds back into Vite's bundle hashes,
which is the likely reason web/frontend/dist/* also churns on rebuilds.

This commit catches the generator up to the actual contracts.ts content.
After this, `go run ./cmd/generate-types` is idempotent against HEAD.

Verified: generator output is byte-identical to contracts.ts.
Adds TestContractsInSync, which runs the generator's content function
and asserts byte-equality with the committed frontend/src/types/contracts.ts.
The next time a contributor hand-edits contracts.ts (or hand-edits the
hardcoded TS string in main.go) without updating both sides, CI fails
with a clear message pointing at the fix:

  Either run \`go run ./cmd/generate-types\` from the module root
  (if the generator is the source of truth) or update the string
  literals in main.go (if contracts.ts is the source of truth).

The drift this test guards against is what allowed PRs smart-mcp-proxy#424 and smart-mcp-proxy#463
to silently leave the generator out of sync.

Refactors main.go to factor out generateFileContent() so the test
can compare without re-implementing the header concat.
@electrolobzik electrolobzik changed the title fix(generate-types): re-sync hardcoded TS output with contracts.ts fix(generate-types): re-sync hardcoded TS output with contracts.ts + drift test May 16, 2026
electrolobzik added a commit to HaloCollar/mcpproxy-go that referenced this pull request May 16, 2026
Brings in the generator-sync + drift-prevention test:
- fc0945c fix(generate-types): re-sync hardcoded TS output with contracts.ts
- f2d90e5 test(generate-types): catch future contracts.ts drift in CI

After this merge, running `go run ./cmd/generate-types` is idempotent
against the committed frontend/src/types/contracts.ts and CI will fail
on any future drift between the two.
@Dumbris
Copy link
Copy Markdown
Member

Dumbris commented May 17, 2026

Thank you for this, @electrolobzik! 🙏 This was a real, recurring papercut — every make build silently reverting 22 lines and dirtying the tree — and the TestContractsInSync guard is exactly the right discipline to stop it recurring.

One snag: the new byte-exact test failed on windows-amd64 CI because core.autocrlf=true checks out contracts.ts as CRLF while the generator emits LF. Since this PR is from a fork branch we can't push to, we've carried your two commits unchanged (authorship preserved) plus a CRLF fix (.gitattributes pin + EOL normalization in the test) into #475, which is now green on all platforms and will ship in v0.31.1. Closing in favor of that — full credit to you for the fix. Much appreciated! 🎉

Dumbris added a commit that referenced this pull request May 17, 2026
…edes #472) (#475)

* fix(generate-types): re-sync hardcoded TS output with contracts.ts

The TypeScript-as-Go-string literal in cmd/generate-types/main.go drifted
from frontend/src/types/contracts.ts when PR #424 (Server Config tab
parity) and PR #463 (per-tool enable/disable) edited contracts.ts
directly without updating the generator. Running `go run ./cmd/generate-types`
(invoked by Makefile's `frontend-build` target) silently reverts those
fields, producing a dirty working tree on every `make build`:

  - Server.isolation_defaults
  - IsolationConfig.network_mode, IsolationConfig.extra_args
  - IsolationDefaults (entire interface)
  - Tool.disabled, Tool.approval_status

The reverted contracts.ts also feeds back into Vite's bundle hashes,
which is the likely reason web/frontend/dist/* also churns on rebuilds.

This commit catches the generator up to the actual contracts.ts content.
After this, `go run ./cmd/generate-types` is idempotent against HEAD.

Verified: generator output is byte-identical to contracts.ts.

* test(generate-types): catch future contracts.ts drift in CI

Adds TestContractsInSync, which runs the generator's content function
and asserts byte-equality with the committed frontend/src/types/contracts.ts.
The next time a contributor hand-edits contracts.ts (or hand-edits the
hardcoded TS string in main.go) without updating both sides, CI fails
with a clear message pointing at the fix:

  Either run \`go run ./cmd/generate-types\` from the module root
  (if the generator is the source of truth) or update the string
  literals in main.go (if contracts.ts is the source of truth).

The drift this test guards against is what allowed PRs #424 and #463
to silently leave the generator out of sync.

Refactors main.go to factor out generateFileContent() so the test
can compare without re-implementing the header concat.

* fix(generate-types): make TestContractsInSync CRLF-safe on Windows

The new TestContractsInSync did a raw byte comparison of the committed
contracts.ts against generator output. On Windows CI (core.autocrlf=true)
git checks out contracts.ts with CRLF endings while the generator emits
LF, so the test failed on windows-amd64 even though the contract was in
sync (observed on PR #472).

Two-layer fix:
- .gitattributes pins frontend/src/types/contracts.ts to `text eol=lf`
  so it is checked out identically on every platform (the real fix).
- The test now normalizes CRLF->LF before comparing, keeping it green
  regardless of a contributor's local git config (defense in depth).

Verified: test passes with both LF and CRLF checkouts of contracts.ts;
`go run ./cmd/generate-types` remains idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Roman Chernyak <electrolobzik@gmail.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
@Dumbris Dumbris closed this in #475 May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants